package hw3.proxy.core;

import static hw3.proxy.core.ConfigurationProperties.AUTO_OPEN_BROWSER;
import static hw3.proxy.core.ConfigurationProperties.ONLY_ALLOW_TEXTUAL_RESPONSES;
import static hw3.proxy.core.ConfigurationProperties.OUTPUT_TO_CONSOLE;
import static hw3.proxy.core.ConfigurationProperties.OUTPUT_TO_FRAME;
import static hw3.proxy.core.ConfigurationProperties.OUTPUT_TO_XHTML_FILE;
import static hw3.proxy.core.ConfigurationProperties.REPLACE_TOKENS;
import static hw3.proxy.core.ConfigurationProperties.XHTML_OUTPUT_FILENAME;
import static hw3.proxy.core.ProxyDBManager.ETAG;
import static hw3.proxy.core.ProxyDBManager.EXPIRES;
import static hw3.proxy.core.ProxyDBManager.LASMODIFIED;
import static hw3.proxy.core.ProxyDBManager.RESPONSE;
import static hw3.proxy.core.ProxyDBManager.URL;
import hw3.exception.StdErrReporterException;

import java.io.IOException;
import java.net.URISyntaxException;
import java.sql.SQLException;
import java.util.Date;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;

/**
 * This class is responsible for getting resources that the client requests. It's main
 * method is getResource() which is basically the implementation of the decision tree that
 * was given in the assignment.
 *  
 * @author Adam Levi
 * @author Marina Skarbovsky
 *
 */
public class ResourceManager
{

	/**
	 * creates a ResourceManager object 
	 * 
	 * @param host The DB host name
	 * @param port The DB port number
	 * @param dbName The DB name at the host
	 * @param username The user name that will be used to connect to the DB
	 * @param password The user name that will be used to connect to the DB
	 * @param tokensSet A String to String mapping to use when ConfigurationProperties.REPLACE_TOKENS = true
	 */
	public ResourceManager(
			String host, int port, String dbName,
			String username, String password,
			final Set<Entry<String, String>> tokensSet)
	{
		m_ProxyDBManager = new ProxyDBManager(host, port, dbName, username, password);
		m_TokensSet = tokensSet;
		m_OriginSrvConnHandler = new OriginSrvConnHandler();
		
		// connecting to DB
		connectToDB();
	}
	
	/**
	 * returns a the resource that is requested in the HTTP request.
	 * 
	 * @param requestToHandle an HTTP request.
	 * 
	 * @return the requested resource in case of success, null in case of error.
	 * if null is returned then an "HTTP/1.1 500 Internal Server Error" message should
	 * be returned to the client.
	 * 
	 */
	public HttpResponse getResource(HttpRequestToHandle requestToHandle)
	{
		m_ResponseIs404Message = false;
		String url = null;
		HttpResponse response = null;
		HttpRequest request = requestToHandle.getRequest();
		try
		{
			url = HttpMsgHandler.getRequestedURL(request);
		}
		catch(Exception e)
		{
			StdErrReporterException.reportErrorFromException(
					ResourceManager.class.getName(),
					"unable to get url from the given request", e);
			return null;
		}
		try
		{
			
			// first we check if there the requested site should be taken from the cache
			if (!HttpMsgHandler.isNoCacheNoStore(request))
			{
				String[] cacheEntry = getCacheEntry(url);
				if (cacheEntry == null) 
				{
					// in this case - the entry does not exist in the cache, 
					// so we forward the request to the origin server.
					// in this point - if we get an error then we must forward
					// the error to the calling class (HttpMsgHendler) so it 
					// will forward "HTTP/1.1 500 Internal Server Error" to the client
					response = getResFromOriginCacheIfNeeded(url, requestToHandle);
				}
				else //the cache exists in the DB
				{
					// we first check if an expiration date exists
					// if exists - we check if it passed or not
					// if it does not exists we will check for a validation mechanism
					Date expirationDate = HttpDateParser.getExpirationTime(cacheEntry[EXPIRES], true);
					if (expirationDate != null)
					{
						if (!HttpDateParser.isDatePassed(expirationDate)) // the Resource did not expire
						{
							response = getResFromCacheEntry(cacheEntry, requestToHandle);
						}
					}
					if (response == null)
					{
						// in this case - the resource either expired or didn't have
						// an expiration date. in this case we will check
						// if there is a validation mechanism - if one exists
						// we will validate the resource against the origin server
						// if there is no validation mechanism - we will ask for
						// a new version of the resource from the origin server
						String eTag = cacheEntry[ETAG];
						String lastModifiedString = cacheEntry[LASMODIFIED];
						if (eTag == null && lastModifiedString == null) // no validation mechanism
						{
							tryDeleteEntry(url);
							response = getResFromOriginCacheIfNeeded(url, requestToHandle);
						}
						else // validation mechanism exists
						{
							HttpResponse newResponseFromOrigin = null;
							try
							{
								newResponseFromOrigin = validateResource(url, eTag, lastModifiedString);
							}
							catch(Exception e)
							{
								StdErrReporterException.reportErrorFromException(
										ResourceManager.class.getName(),
										"error while validating: " + url + " attempting direct service", e);
								response = getResFromOriginCacheIfNeeded(url, requestToHandle);
							}
							if (newResponseFromOrigin == null) // the resource is valid
							{
								response = getResFromCacheEntry(cacheEntry, requestToHandle);
							}
							else // the resource is NOT valid
							{
								// deleting old entry
								tryDeleteEntry(url);
								
								// caching the new response if needed and checks other conditions
								response = handleHttpResponseCacheIfNeeded(url, requestToHandle, newResponseFromOrigin);	
							}
						}
					}
				} //the cache exists in the DB
			}
			else // the request contains either: NoCache or NoStore headers
			{
				response = getResFromOriginCacheIfNeeded(url, requestToHandle);
			}
		}
		catch(Exception e)
		{
			StdErrReporterException.reportErrorFromException(
					ResourceManager.class.getName(),
					"error while serving: " + url + " attempting direct service", e);
			response = getResFromOriginCacheIfNeeded(url, requestToHandle);
		}
		if (
				REPLACE_TOKENS &&
				response != null &&
				!m_ResponseIs404Message && /* not sending our 404 message */
				HttpMsgHandler.isTokenReplacementAllowed(response))
			
		{
			return getNewTokensResponseForClient(url, response);	
		}
		return response;
	}
	
	/**
	 * A wrapper method that construct the cache table. 
	 */
	public void constructCacheTables() throws SQLException, StdErrReporterException, ClassNotFoundException
	{
		m_ProxyDBManager.constructCacheTables();
	}

	/**
	 * A wrapper method that deletes the cache table. 
	 */
	public void deleteCacheTables() throws SQLException, StdErrReporterException, ClassNotFoundException
	{
		m_ProxyDBManager.deleteCacheTables();
	}

	/**
	 * A wrapper method that clears the cache table. 
	 */
	public void clearCache() throws SQLException, ClassNotFoundException
	{
		m_ProxyDBManager.clearCache();
		
	}
	
	/**
	 * A wrapper method that poses a query
	 * @param query The query to pose
	 */
	public void poseQuery(String query) throws ClassCastException, StdErrReporterException, SQLException, ClassNotFoundException, InstantiationException, IllegalAccessException, IOException, URISyntaxException
	{
		m_ProxyDBManager.poseQuery(
				query, 					// the requested query
				OUTPUT_TO_CONSOLE,  	// should we output to the console ?
				OUTPUT_TO_FRAME,  		// should we output to a swing frame ?
				OUTPUT_TO_XHTML_FILE,	// should we output to an XHTML file ?
				XHTML_OUTPUT_FILENAME,	// what is the name of the XHTML file ?
				AUTO_OPEN_BROWSER);		// should we auto-open the browser ?
		
	}
	
	/**
	 * Sets a new token list to use when token replacement is on. 
	 * @param tokensSet
	 */
	public void setTokensSet(Set<Entry<String, String>> tokensSet)
	{
		m_TokensSet = tokensSet;
	}
	
	/**
	 * terminating the resource manager. 
	 */
	public void terminate()
	{
		//m_OriginSrvConnHandler.terminate() ?

		m_TokensSet = null;
		try
		{
			m_ProxyDBManager.terminate();
		}
		catch (Exception e)
		{
			StdErrReporterException.reportErrorFromException(
					ResourceManager.class.getName(),
					"error while creating a response message from the string with the new tokens", e);
		}
		
	}
	
	/**
	 * Creates a response for the client. The method takes an HTTP response and checks
	 * if it a token replacement procedure should be taken on it,
	 * if so - it will try and build a new response that after making the token replacement
	 * procedure, if not - it will return the original response that was given.
	 * 
	 * @param url The URL of the original resource 
	 * @param originalResponse The HttpResponse that represents the resource
	 * @return an HTTP response (either one that had a token replacement procedure taken on it
	 * or the original one in case of errors or in case no procedure should be taken)
	 */
	HttpResponse getNewTokensResponseForClient(String url, HttpResponse originalResponse)
	{
		String[] cacheEntry = getCacheData(url, originalResponse);
		if (cacheEntry == null) // unable to create cache entry
		{
			return originalResponse;
		}
		else
		{
			try
			{
				String responseStr = cacheEntry[RESPONSE];
				String newResponseStr = getNewTokensResource(responseStr);
				return HttpMsgHandler.createResponseFromString(newResponseStr);
			}
			catch(Exception e)
			{
				StdErrReporterException.reportErrorFromException(
						ResourceManager.class.getName(),
						"error while creating a response message from the string with the new tokens", e);
				return originalResponse;
			}
		}
	}
	
	/**
	 * creates a resource which have all its' tokens from the mapping
	 * m_TokensSet replaced with their values. for instance: suppose
	 * m_TokensSet contains the map (some-text -> new-text) then all
	 * occurrences of some-text in the resource BODY (IE. all the occurrences
	 * that appear after a double new-line) will be replaced with new-text.
	 *   
	 * @param resource a string representation of an HTTP message 
	 * @return a resource to send to the client
	 */
	String getNewTokensResource(String resource)
	{
		if (m_TokensSet == null)
		{
			return resource;
		}
		int bodyStart = resource.indexOf(HeaderBodySeperator) + HeaderBodySeperator.length();
		String Header = resource.substring(0, bodyStart);
		String Body = resource.substring(bodyStart);
		for (Entry<String, String> entry : m_TokensSet)
		{
			Body = Body.replaceAll(entry.getKey(), entry.getValue());
		}
		return Header + Body;
	}
	
	/**
	 * Deletes the given cache entry from the DB, doesn't throw exceptions
	 * 
	 * @param url A URL that is the Internet location of a resource
	 */
	void tryDeleteEntry(String url)
	{
		try
		{
			m_ProxyDBManager.deleteRow(url);
		}
		catch (Exception e)
		{
			StdErrReporterException.reportErrorFromException(
					ResourceManager.class.getName(),
					"error while deleting the entry: " + url, e);
		}	
	}
	
	/**
	 * Retrieves a response from a cache entry. if an error has occurred while
	 * creating the response from the given cache entry then a request for
	 * the resource to the origin server will be made.
	 * 
	 * @param cacheEntry an array representing a cache entry
	 * @param requestToHandle the original request that was made by the client
	 * 
	 * @return an HttpResponse to deliver to the user in case of success,
	 * otherwise null.
	 */
	HttpResponse getResFromCacheEntry(String[] cacheEntry, HttpRequestToHandle requestToHandle)
	{
		try
		{
			return HttpMsgHandler.createResponseFromString(cacheEntry[RESPONSE]);
		}
		catch(Exception e)
		{
			// an error has occurred while creating a response message,
			// although unlikely to happen - if this is the case
			// we delete the cached entry and we will contact
			// the origin server for the resource
			StdErrReporterException.reportErrorFromException(
					ResourceManager.class.getName(),
					"error while creating a response message from the cache", e);
			return getResFromOriginCacheIfNeeded(cacheEntry[URL], requestToHandle);
		}
	}
	
	/**
	 * Tests if the given response it a textual response.
	 * 
	 * @param response An HTTP Response
	 * @return true if and only if the response it Textual
	 */
	boolean isResponseTextual(HttpResponse response)
	{
		try
		{
			return HttpMsgHandler.isResponseTextual(response);
		}
		catch(Exception e)
		{
			StdErrReporterException.reportErrorFromException(
					ResourceManager.class.getName(),
					"error while testing if the response is textual, assuming not textual", e);
			return false;
		}	
	}
	
	/**
	 * Handles the HTTP Response from the origin server. the response will be tested whether it
	 * should be cached or not. and if ConfigurationProperties.ONLY_ALLOW_TEXTUAL_RESPONSES == true
	 * and the response is not textual then a 404 response will be returned instead of the original
	 * Response from the origin
	 * 
	 * @param url The resource's URL
	 * @param requestToHandle The original request from the client
	 * @param response The HTTP response from the origin
	 * @return an HTTP Response to deliver to the client
	 */
	HttpResponse handleHttpResponseCacheIfNeeded(
			String url, HttpRequestToHandle requestToHandle, HttpResponse response)
	{
		// if token replacement is not allowed then we just forward the response
		// to the client
		if (!HttpMsgHandler.isTokenReplacementAllowed(response))
		{
			return response;
		}

		String[] cacheEntry = getCacheData(url, response); // getCacheData() never throws exceptions
		boolean isResponseTextual = isResponseTextual(response);
		if (ONLY_ALLOW_TEXTUAL_RESPONSES)
		{
			// if we only allow textual response
			// and the response is not textual
			// then we won't store it in the cache
			// and we will send a 404 message to the client
			if (!isResponseTextual)
			{
				return get404Message(requestToHandle); // never fails
			}
		}

		// note: in any case: we only allow textual responses inside the DB
		// check if the response should be cached
		if (	cacheEntry != null 	&&			// creating cache entry succeeded
				isSatisfyCacheCond(response) && // && the response should be cached
				isResponseTextual)				// && the response is textual	
		{
			// update the DB with the new URL
			// if we fail here then we will just pass the
			// response from the origin forward
			try
			{
//				String headers = cacheEntry[RESPONSE];
//				headers = headers.split(ResourceManager.HeaderBodySeperator)[0];
//				System.out.println("Entering to table:" + headers);
				m_ProxyDBManager.updateRow(cacheEntry);
			}
			catch(Exception e)
			{
				StdErrReporterException.reportErrorFromException(
						ResourceManager.class.getName(),
						"error while writing the cache entry to the DB", e);
			}
		}

		// in any case we forward the response to the caller
		return response;
	}
	
	/**
	 * Retrieves a response from the origin server. if the response should be
	 * cached then an attempt to cache it will be made.
	 *
	 * @param url The URL of the required resource.
	 * @param requestToHandle an HTTP request to handle
	 * 
	 * @return an HttpResponse to deliver to the user in case of success, otherwise null.
	 */
	HttpResponse getResFromOriginCacheIfNeeded(String url, HttpRequestToHandle requestToHandle)
	{
		HttpResponse response;
		try
		{
			response = getResFromOrigin(requestToHandle.getRequest());
		}
		catch(Exception e)
		{
			StdErrReporterException.reportErrorFromException(
					ResourceManager.class.getName(),
					"error while getting resource from origin server", e);
			return null;	
		}
		return handleHttpResponseCacheIfNeeded(url, requestToHandle, response);
	}
	
	/**
	 * Gets a 404 message
	 * @param requestToHandle The original request from the client
	 * @return a 404 proxy response
	 */
	HttpResponse get404Message(HttpRequestToHandle requestToHandle)
	{
		try
		{
			HttpResponse response = HttpMsgHandler.create404Response(requestToHandle);
			m_ResponseIs404Message = true;
			return response;
		}
		catch(Exception e)
		{
			StdErrReporterException.reportErrorFromException(
					ResourceManager.class.getName(),
					"error while creating 404 message", e);
			return null;		
		}
	}

	/**
	 * creates an array of strings that represents a cache entry
	 * @param url The Resource's URL
	 * @param response The HTTP Response of the URL
	 * @return an array of strings that represents a cache entry in case of
	 *  success otherwise null
	 */
	String[] getCacheData(String url, HttpResponse response)
	{
		try
		{
			return HttpMsgHandler.createCacheEntry(url, response);
		}
		catch(Exception e)
		{
			StdErrReporterException.reportErrorFromException(
					ResourceManager.class.getName(),
					"error while creating cache data from the HTTP response", e);
			return null;	
		}
	}
	
	/**
	 * Retrieves a resource from the origin server
	 * 
	 * @param request An HTTP Request from the user
	 * @return a Resource from the origin server
	 */
	HttpResponse getResFromOrigin(HttpRequest request) throws IOException, HttpException
	{
		HttpRequest requestForOrigin = HttpMsgHandler.createRequestForOriginSrv(request);
		return m_OriginSrvConnHandler.getResponseToRequest(requestForOrigin);
	}
	
	/**
	 * Retrieves an array that represents a cache entry from the DB
	 * @param url The resource's URL
	 * @return An array that represents a cache entry from the DB
	 */
	String[] getCacheEntry(String url)
	{
		try
		{
			return m_ProxyDBManager.getCacheEntry(url);
		}
		catch(Exception e)
		{
			StdErrReporterException.reportErrorFromException(
					ResourceManager.class.getName(),
					"error while retreiving the resource from the cache", e);
		}
		return null;
	}
	
	/**
	 * Tests if the given HttpResposne should be cached
	 * @param response An HTTP response
	 * @return true if and only if the given HttpResposne should be cached
	 */
	boolean isSatisfyCacheCond(HttpResponse response)
	{
		try
		{
			return (HttpMsgHandler.isResponseSatisfyCacheCond(response));
		}
		catch(Exception e)
		{
			StdErrReporterException.reportErrorFromException(
					ResourceManager.class.getName(),
					"error while checking if the response should be cached, response will not be cached", e);
			return false;
		}
	}
	
	/**
	 * Tries to connect to the DB
	 */
	void connectToDB()
	{
		try
		{
			m_ProxyDBManager.connect();
		}
		catch (Exception e)
		{
			StdErrReporterException.reportErrorFromException(
					ResourceManager.class.getName(),
					"error while connecting to the DB", e);
		}
	}
	
	/**
	 * Tests if the given resource if valid.
	 * 
	 * @param url A URL that is the Internet location of the resource
	 * @param eTag An ETAG string
	 * @param lastModifiedString a Last Modified string for the URL
	 * 
	 * @return true if and only if the resource pointed by the URL is valid according to
	 * the origin server
	 */
	HttpResponse validateResource(String url, String eTag, String lastModifiedString) throws IOException, HttpException
	{
		HttpResponse newResponse = m_OriginSrvConnHandler.verifyCacheEntry(
				url, eTag, lastModifiedString);
		return newResponse;
	}
	
	/**
	 * The characters that separate an Header from it's body in an HTTP message
	 */
	static final String HeaderBodySeperator = "\r\n\r\n";

	boolean						m_ResponseIs404Message;
	ProxyDBManager 				m_ProxyDBManager;
	Set<Entry<String, String>> 	m_TokensSet;
	OriginSrvConnHandler 		m_OriginSrvConnHandler;

}
